Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts: # build.sh
# Conflicts: # build.sh # wlx/mpv_wayland/src/mpvwidget.cpp
# Conflicts: # wlx/logview/src/LogViewerWidget.cpp
- Move action configuration into completed handler so connections are established after the KPart is fully loaded - Use dynamic property guard (_kpw_connected) to prevent duplicate lambda connections (Qt::UniqueConnection doesn't work with lambdas) - Persist checkable action states to QSettings INI file - Restore settings using trigger() to change Gwenview's internal zoom state, not just checkbox visual state - Handle zoom radio pair specially during restore to avoid Gwenview fighting back when we manipulate checkbox states directly - Add Ctrl+Shift+S shortcut routing for Save As - Remove aggressive uncheck→check-other radio logic that conflicted with manual zoom in/out (custom zoom is a valid state)
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
…n body has zero effect in Qt's QTextDocument
Root-caused via direct measurement, not guesswork: rendered the same
paragraph at body{font-size:50%/100%/182%} in a real QTextDocument and
measured identical text width in all three cases. Qt's simplified rich-
text CSS engine does not apply a percentage font-size on <body> to
descendant text at all -- the previous 'it worked' observation after
Save Zoom was the leftover native QTextBrowser wheel-zoom (never
actually cleared, only its counter was zeroed) coincidentally still
visually applied, not the CSS mechanism.
Reverted the CSS-injection approach in the core engine entirely
(renderFileToHtml/renderTextToHtml/renderMarkdown/postProcessHtml back
to their pre-zoom 3-arg signatures) and replaced it with each toolkit
applying persisted zoom via its own native, proven-working zoom API
instead:
- Qt6: absolute font-size approach via QFont::setPointSizeF() on the
widget PLUS QTextDocument::setDefaultFont() -- confirmed live that
setFont() alone does not retroactively rescale content already loaded
via setHtml() (measured zero width change), only setDefaultFont()
does. wheelEvent/saveZoom/resetZoom all route through one
applyZoom() method now instead of Qt's own relative zoomIn/zoomOut,
avoiding any need for careful increment/decrement bookkeeping.
- GTK3: webkit_web_view_set_zoom_level() applied directly after each
load, in the exact same call already used for the transient
scroll-zoom (onScroll) -- 'Save Zoom' just makes that setting survive
a reload/reopen instead of introducing a second, CSS-based path.
Also removed the temporary diagnostic logging added while chasing this
(root cause found and fixed) and the now-superfluous exception-safety
fprintf calls' surrounding investigation notes are left as permanent
documentation of why this file avoids std::regex and needs try/catch
at the extern "C" boundary.
Confirmed live that neither QTextBrowser::zoomIn/zoomOut nor the absolute setFont()+setDefaultFont() approach applyZoom() already uses for text touch <img> sizing at all (measured identical idealWidth before/after a zoom change with an explicitly-sized image present). Added explicit QTextImageFormat width/height scaling: each image's natural size is captured once right after setHtml() (before any zoom is applied), then scaleImages() always computes the new size relative to that cached natural size rather than the document's current (possibly already-zoomed) state, so repeated applyZoom() calls -- every wheel notch -- never compound. GTK3 needs no equivalent change: webkit_web_view_set_zoom_level() is a real browser engine's page zoom, which already scales images along with text as standard behavior, unlike Qt's text-only zoom.
…so zoom never scaled them renderMathTag()'s <img> tag was emitted with no width/height attributes at all, unlike renderDiagramImgTag() (mermaid/plantuml) which already sets them from its own w/h. Without explicit attributes, QTextImageFormat has nothing to read a natural size from -- Qt6's scaleImages() (added in 50a1893) reads back 0x0 and skips the image entirely, so LaTeX formulas never scaled with the rest of the zoom. renderLatexToPng's w/h out-params are already the PNG's own logical (post-8x-oversampling) pixel size -- same value the caller already had available, just never wrote into the tag. Adding it doesn't change the formula's rendered size, only makes it explicit for scaleImages() to read.
- Remove mvLog() tracing entirely (both definitions -- markdown_engine.cpp and diagram_render.cpp had separate copies writing to the same scratch log file -- and all ~20 call sites across renderDiagramImgTag, renderMathTag, renderMarkdown, init, renderFileToHtml, and svgToHighDpiPng's degenerate-size fallback paths) - Remove the two GTK3 fprintf debug lines in isSystemDark()/ resolveDarkMode() explicitly marked as temporary diagnostics for an already-resolved theme-detection question Left the ListLoad exception-handler fprintf(stderr, ...) calls in both toolkits' catch blocks alone -- permanent crash-safety error reporting matching the pattern already used elsewhere in this repo (e.g. diagramview_gtk3), not debug tracing.
…age document viewer svgview: Qt6/GTK3 SVG lister plugin backed by librsvg+cairo, with zoom/pan/export-to-PNG and a fixed ListLoad/ListCloseWindow parent-widget handling bug that crashed the Qt6 build's right-click popup under Wayland. pdfview: Qt6/GTK3 lister plugin for PDF/EPUB/MOBI/FB2/XPS/CBZ (mupdf) and DjVu (djvulibre), with: - continuous document-length scroll with lazy per-page rendering/eviction - HiDPI-correct rendering (mupdf re-rasterizes at devicePixelRatio/scale_factor) - row-aware, cross-page text selection using mupdf's real line grouping - an in-document find bar (live search, match count, Prev/Next, F3/Shift+F3) - right-click context menu (copy selection/page text, print, find) - lc_focus wiring so keyboard shortcuts and wheel-based navigation work when embedded in Double Commander's Lister view
…focus The Qt6 wrapper is a genuine embedded QWidget, but sits across a native window boundary, so DC's own hotkey manager (Ctrl+Q closes Quick View) never saw the key event -- it was simply dropped, not just unhandled by us. Explicitly detect it and repost KeyPress/KeyRelease to DC's own top-level window, matching the established kpartview/logview pattern for the same problem. The GTK3 wrapper already worked (Ctrl+Q was never in its handled-combos list, so it already fell through to `return FALSE` and propagated normally, same as mpv_wayland's GTK3 plugin does for the same key) -- just added a comment documenting that's intentional.
…rendering
Multiple features/fixes to the markdownview plugin (Qt6 + GTK3), landed
together as one coherent unit of work:
1. Math font selection (8 embedded OpenType MATH fonts)
Vendored MicroTeX is now upstream's "openmath" branch (github.com/
NanoMichael/MicroTeX/tree/openmath), replacing master -- master has no
working OpenType MATH-table support at all (an otf2clm.py script exists
but nothing consumes it), confirmed via a real build+render test of the
openmath branch before committing to the switch.
8 math fonts (Latin Modern Math, IBM Plex Math, STIX Two Math,
Libertinus Math, Fira Math, DejaVu Math TeX Gyre, TeX Gyre Pagella Math,
Euler Math) are embedded directly into the compiled .wlx binary and
self-seeded to ~/.config/doublecmd/markdownview_fonts/ on first use,
plus auto-discovery of any hand-dropped .otf/.clm1 pair in that
directory. New "Math Font" context-menu submenu in both toolkits, ini
setting math_font, persists across restarts.
Font selection is by .clm1 file path, not display name -- MicroTeX
registers each font under a name read from the font file's own metadata
(e.g. "LatinModernMath-Regular"), which frequently isn't the same
string a menu would show ("Latin Modern Math"). Passing an unresolved
name straight to MicroTeX::parse() doesn't fail gracefully: confirmed
live via a debug build + gdb backtrace that it segfaults deep inside
env.upem() on a null font pointer. Selecting by path sidesteps
name-matching entirely and lets any unresolved/invalid selector
collapse to the same "use the default font" case as no selector at all.
2. Ctrl+Q didn't close Quick View when the Qt6 plugin had focus
Same root cause as wlx/pdfview's identical fix: the Qt6 wrapper is a
real embedded QWidget, but sits across a native-window boundary, so
DC's own hotkey manager never saw the key event at all. Explicitly
catch Ctrl+Q and repost it to DC's own top-level window. GTK3 already
worked (Ctrl+Q was never in its handled-combos list); added a comment
documenting that's intentional.
3. Image zoom scaling
LaTeX/diagram <img> tags gained explicit width/height attributes (the
PNG's own logical post-oversampling size) so Qt6's zoom feature -- which
reads QTextImageFormat's natural size to compute a scaled size -- can
see and scale them; previously they read back 0x0 and were skipped
entirely.
4. ```chart fenced code blocks (line/bar/scatter, grouped + stacked bars)
Renders a JSON chart spec (same shape ~/repos/reports' charts.py uses,
so a document written for that pipeline renders unchanged here) to a
PNG via Cairo -- no external process, no network, no Python. Cairo is
already a shared dependency of markdownview_core on both toolkits (see
diagram_render.cpp's SVG rasterization), so this needed no per-toolkit
backend split. New nlohmann/json single-header dependency for spec
parsing.
Supports line/bar/scatter (not pie), multi-series with a legend,
grouped bars by default for multiple series, stacked:true for stacked
bars, stacked:"percent" for 100%-stacked bars. Malformed JSON or an
unsupported type falls back to the plain fenced code block, same
contract as the mermaid/plantuml renderers.
Chart text (axis/tick/legend/title) mirrors the document's own active
CSS: body's font-family for body text, the heading rule's font-family
and font-weight for the title -- read from whichever stylesheet
(custom theme, markdownview.css, or the built-in default) is actually
resolved for the render, via a hand-written CSS block/property scanner
(this file already avoids std::regex entirely -- a real, confirmed-live
SIGSEGV in libstdc++'s regex locale setup, not a style preference).
Fixed one real bug found via user report + screenshot during
development: bar charts with numeric (not categorical-string) x values
got a too-small axis padding, causing bars to spill past the y-axis and
off the canvas edge entirely with closely-spaced x values -- bar-width
math now always derives padding from actual x-spacing regardless of
whether x is numeric or categorical.
All changes verified end-to-end through the real production render path
(not just unit-level), including multiple debug-build + gdb sessions to
root-cause live crashes rather than guessing.
TfrmMain's TToolBar/TToolButton row is replaced with a MainMenu (Plugins/ Tools/Settings/Help menus mirroring the existing actions) plus a TPanel-based button row (regular TButton controls instead of toolbar buttons), split into pnlMiddle (plugin action buttons) and pnlBottom. Adds an Exit button/menu item (btnExit/miExit -> Close).
…nels
Expands ```chart rendering to match ~/repos/reports' charts.py's current
scope, which has grown from the original line/bar/scatter subset to all 13
matplotlib 2D mark types plus a layering/multi-panel composition mechanism.
Rendering is still native Cairo -- no matplotlib, no Python, no network --
mirroring charts.py's types and JSON spec shape, not its implementation.
Architecture mirrors charts.py's own: one extent function + one draw
function per mark type (line, bar, barh, scatter, area, step, stem,
errorbar, histogram, boxplot, violin, heatmap, pie), dispatched by type
string. A panel's "layers" list draws several mark types onto one shared
axis system in order (each layer's extent function extends a running
bounding box, exactly mirroring matplotlib's own per-artist autoscale
union) -- this is what composite figures like a dumbbell (thin barh
connector + two scatter points) or a timeline (barh duration bars + scatter
pins + annotations) are built from, with no new renderer code needed.
Top-level "panels" stacks several such panels vertically with shared_x
support. Cross-cutting log_x/log_y, ref_lines, ref_bands, and annotations
apply to any panel's shared coordinate system.
heatmap and pie have no shared x/y coordinate system at all (a pixel grid;
a radial layout) and are only supported as a panel's sole layer, rendered
through their own self-contained path -- documented in the file's top
comment. Heatmap uses a hand-picked viridis colormap approximation
(control-point interpolation, not byte-exact to matplotlib's).
type:"bar" retains this plugin's existing stacked:"percent" addition
(100% stacked bars) beyond charts.py's own plain stacked bool.
Found and fixed 5 real bugs during verification, each confirmed via an
actual rendered PNG through the real production render path (not just code
review):
- Duplicate title on single-panel specs: the figure-level title and the
panel's own title read the same JSON field, drawing it twice. Now only
treated as a separate figure suptitle when "panels" is actually used,
matching charts.py's own `if panels: fig.suptitle(...)`.
- Boxplot whiskers overshot to an outlier instead of stopping at the
correct Tukey (1.5*IQR) limit -- lo/hi were seeded from the RAW
min/max (outliers included) before the exclusion loop ran, so an
outlier with nothing smaller/larger on its side left the whisker
stretched all the way to it.
- Violin curves clipped against the plot's top/bottom edge -- the axis
extent only accounted for the raw data min/max, not the KDE's actual
padded draw range (bandwidth + 15% padding on each side).
- Log-scale axes rendered almost entirely blank -- the padding added to
the axis range was linear-additive regardless of scale, which could
push a log axis's padded minimum negative; clamping that to a
near-zero epsilon before log10() exploded the log-space range to
hundreds of decades, collapsing the whole curve into a sliver at one
edge. Log axes now pad multiplicatively in log-space instead.
- Heatmap yticklabels ("Y1"/"Y2"/"Y3") clipped against the canvas edge --
the left margin was a fixed 15px regardless of label width; now
measured from the actual widest label.
Violin uses a simplified Gaussian KDE (Silverman's rule-of-thumb
bandwidth) -- not matplotlib's exact algorithm, but the same fundamental
technique, visually close enough for a report chart.
README was stale on several fronts: still described the old clatexmath system-font-file requirement (obsolete since the MicroTeX openmath-branch upgrade), didn't mention math font selection, chart rendering, the Ctrl+Q fix, or image zoom scaling at all, and the Context Menu bullet only listed a subset of what's actually there. Also the ini config block was missing zoom_multiplier and math_font, both already-persisted settings with no documentation anywhere. Adds dedicated Math Font Selection and Chart Rendering sections (spec shape, examples, layering/multi-panel/cross-cutting fields, the percent-stacked bar addition beyond charts.py's own spec).
…bugs
Rendered every ```chart block from a real, non-synthetic report through
the actual plugin and compared each against its evident intent. Found and
fixed 9 real bugs, several serious:
- Categorical (string) x/y values were essentially unsupported outside a
layer's own array -- scatter "points", and annotations, silently dropped
any string coordinate. Broke a dumbbell composite (missing scatter
endpoints), a timeline (missing pins/annotations, dropped at PARSE time,
before rendering ever ran), and made an entire scatter chart render
completely blank (all-string x-axis). Fixed with a per-panel
CategoryRegistry shared across every layer's extent and draw pass, so
any layer/point naming a category another layer defined resolves to the
same position -- this is what actually makes "layers" composites work,
not just render without crashing.
- barh bar heights were computed from a category count read mid-loop, as
it grew -- early bars rendered at nearly full plot height and overflowed
off the canvas.
- log_y was silently disabled by bar/stem's forced 0-baseline (0 is never
valid on a log axis); forcing it broke log mode entirely.
- Log-scale vertical bars ballooned past the plot -- baseline used y=0
unconditionally, which under a log transform maps to a wildly
out-of-range pixel position.
- Categorical tick labels clipped against the canvas edge (fixed margins
instead of measuring actual label width).
- ref_bands labels were silently dropped from the legend (ref_lines added
themselves; ref_bands never did).
- errorbar's asymmetric [[lower...],[upper...]] format (matplotlib's own
convention) was silently read as empty by the flat-array parser.
- Multi-line annotation text ("\n") rendered as a tofu glyph instead of a
line break -- cairo_show_text has no newline concept.
- Open/unfilled scatter markers ignored their own declared color, always
stroking in the plain foreground text color.
- First/last x-axis tick labels could overflow the canvas horizontally
when centered exactly on the plot edge.
All fixes verified against the real document's actual rendered PNGs
through the production render path, not synthetic re-tests.
A Qt6 WLX lister that previews archive contents without entering the archive: format, member tree, sizes, permissions, link targets, per-entry packed size and CRC, and encryption indicators. Written as a replacement for the third-party libarchive_qt_crap plugin, whose disqualifying flaw was shell injection on the previewed file's name: it built a "/bin/sh -c" command line to scrape an archive comment out of 7z piped through pcregrep, escaping only spaces and single quotes. A file named x$(...).zip executed on F3. This plugin spawns no processes at all. The archive comment comes from parsing the ZIP End of Central Directory record, which also yields the per-entry packed size and CRC that libarchive does not expose. The absence of any subprocess API is enforced at build time by cmake/CheckNoSubprocess.cmake rather than left to review. Other properties the predecessor lacked: * The libarchive walk runs on a worker thread, streaming entries to a QAbstractItemModel in batches, so the file manager stays responsive. Cancellation interrupts libarchive from inside its read callback, which is what makes closing a solid archive mid-decompression instant. * Format detection is synchronous and bounded, so ListLoad can decline a non-archive and let DC fall through to another viewer. * Sizes are handled as int64 throughout; unset means blank, not SIZE_MAX. * Member names are decoded UTF-8-first with locale and configurable codepage fallbacks, never an implicit fromUtf8. * Traversal, absolute, and duplicate member paths are displayed verbatim, and refused on extraction by three independent layers of checks. * 13 WLX entry points including ListLoadNext, all crash-guarded, with a real detect string; only those symbols are exported. Includes a fixture corpus and a 61-check suite covering hostile archives, damaged input, ZIP64, encryption, extraction containment, and the exported C ABI driven as DC drives it. Clean under ASAN and UBSAN.
A Qt6 lister that renders HTML with Chromium rather than QTextBrowser's rich-text subset, so pages written after about 2005 -- grid, flexbox, SVG, web fonts, rowspan tables -- preview as written. Hardened for a preview pane rather than trusted as a browser: * JavaScript, plugins, WebGL, screen capture, the PDF viewer, fullscreen, autoplay and page icons are all off. The profile is off-the-record, so no cookie jar, cache or local storage outlives the preview. * Every request passes a gate that denies anything off the machine until explicitly allowed, with a notice bar reporting how much was blocked. * Local reads are confined to the document's own directory, which no QtWebEngine setting enforces on its own -- a preview pane has no business reading ~/.ssh through an <img src>. * The charset is resolved before the engine sees the file (BOM, XML declaration, meta charset/http-equiv, UTF-8 validity, configurable fallback) and the document is transcoded and handed over as text, so an undeclared legacy page renders instead of turning into mojibake. * Binaries named .html, markup-free files, empty files and anything past the size cap return a null handle, letting DC fall through to another lister instead of showing an empty pane. The usual objection to QtWebEngine in a plugin -- that QWebEngineView cannot be constructed once the host's QApplication already exists -- was tested against a harness reproducing what DC does (QApplication, dlopen, ListLoad, reparent) on Qt 6.11 and does not hold. Charset probing lives in a Qt-free core with its own test binary. build.sh treats the build as optional for the same reason as kpartview: qt6-webengine is a large package and is not present on every runner.
Second edition of the HTML lister for GTK builds of Double Commander, sharing src/core's probe (sniffing, charset resolution, iconv transcode) and one htmlview.ini with the Qt6 edition -- the GTK settings writer emits QSettings' IniFormat quoting so a file written by either is valid for the other. The request gate is the piece with no direct equivalent: WebKitGTK has nothing like QWebEngineUrlRequestInterceptor. WebKit's pre-request hook in the UI process is "resource-load-started", which hands over the WebKitURIRequest before dispatch and allows it to be repointed at about:blank -- there is no cancel. Verified against a live local HTTP server that this is genuinely pre-dispatch: a blocked request leaves the server log empty. Same confinement as the Qt6 edition, file reads restricted to the document's own directory, remote content off until the notice bar's toggle is used. allow-file-access-from-file-urls is ON for the same reason LocalContentCanAccessFileUrls is on the Qt6 side: it is a coarse switch applied before anything reaches the gate, so leaving it off blocks the document's own same-directory resources invisibly. The gate stays the single visible authority. Reentrancy constraints ported from markdownview_gtk3 rather than rediscovered: never call webkit_web_view_load_html() while a load is in flight, defer every reload and menu action to g_idle_add rather than run it inside a signal handler's emission, guard duplicate "destroy" with a live-instances set, and pass the real event to gtk_menu_popup_at_pointer. ListSearchText bridges DC's synchronous API to WebKitFindController with a bounded nested main loop and an in-flight guard. Verified in a harness using a GtkLayout parent (what DC's ResizeWindow requires): renders grid/flexbox/rowspan tables identically to the Qt6 edition, declines binaries and markup-free files with a null handle, blocks a path-escaping <img src>, decodes undeclared cp1252 correctly, and ListLoadNext swaps the document in place.
Moves the scanner, extractor, ZIP central-directory parser, passphrase broker, name handling and settings into src/core/ + include/core/, expressed in std::string, std::thread and std::condition_variable with no Qt or GTK types. Verified: the core objects reference zero Q*, g_* and gtk_* symbols. This is the prerequisite for a GTK3 variant, and follows the convention csvview, dbview, structview and logview already use. The alternative -- duplicating the core per toolkit -- was rejected outright: the central directory parser and the three-layer extraction path checks are security-relevant, and two copies become two behaviours. The Qt6 side is now a set of thin adapters. ArchiveScanner and ArchiveExtractor turn core callbacks into signals; because the core invokes them on its worker thread and the adapters live on the GUI thread, Qt::AutoConnection resolves each emit to a queued delivery, so slots still run on the GUI thread exactly as they did when the scanner was a QThread. ArchiveModel holds archiveview::Entry directly and converts to QString once at insertion rather than per data() call. Behaviour is unchanged: all 61 checks pass, including hostile archives, ZIP64, encryption, extraction containment and the exported C ABI. Notable replacements, each a deliberate choice rather than a mechanical translation: * QStringDecoder -> iconv, which is what glib uses underneath and is in libc on the platforms this targets, keeping the core dependency-free. UTF-8 validation is hand-rolled and rejects overlong forms, surrogates and out-of-range code points. * QSettings -> a hand-written ini reader, so the two variants cannot drift into parsing the same file differently. This also fixes the QSettings quirk where a comma-separated value arrived as a QStringList and read back as an empty string. * QFile -> stdio with fseeko/ftello, preserving large-file support. * QThread/QMutex/QWaitCondition -> std::thread and a mutex plus condition variable. ThreadSanitizer, which was not meaningful against the Qt primitives, now runs clean over scan, cancel-mid-decompression, encrypted listing and extraction.
Drag-and-drop never started. ArchiveModel did not override flags(), so items lacked Qt::ItemIsDragEnabled -- which QAbstractItemView checks before it will call startDrag() at all. The drag implementation was therefore unreachable code, however the view was configured. Confirmed by asserting the flag on a populated model: NO before, YES after. Extraction could freeze Double Commander. Three compounding causes, all addressed rather than guessing which one fired: * The wait was a `while (!finished) processEvents()` spin with no exit if the terminal signal never arrived, so any failure to emit it presented as a frozen file manager instead of an error -- the worst failure mode for the one operation here that writes to disk. Replaced with a real QEventLoop that quits on the signal. * The progress dialog was Qt::WindowModal against a top-level window that belongs to Double Commander, not to Qt. Qt's modality cannot reason about a foreign LCL window; blocking on it is a good way to wedge the host. It is now non-modal, and the view is disabled for the duration instead, which is the containment actually wanted. * The directory chooser was the native one, which goes out to the desktop portal. A portal dialog raised from a plugin inside a non-Qt host is a known hang. Now uses Qt's own dialog. Also adds a re-entrancy guard: extraction runs a nested event loop, so a second request from a menu click or a drag while one is in flight would have nested two loops over one extractor. Detect string: drop JAR and EPUB, add EAR and ACE as requested. Note libarchive has no ACE reader, so .ace files are probed, declined, and handed back to DC for another viewer to handle. Removes the right-hand detail panel and its ini key; the window is now just the contents grid, filter box and status bar.
Adds a GTK3 build of the lister alongside the Qt6 one. Both are driven by the same archiveview::Scanner and the same archiveview::EntryTree, so the decisions about what the user is told -- hostile member names shown verbatim, duplicate paths given their own rows, synthesised directories, packed sizes and CRCs from the ZIP central directory, encryption indicators -- exist once and cannot drift apart. Two pieces moved into the core to make that true rather than aspirational: * EntryTree, the member hierarchy, previously built inside the Qt model. It brackets each group of sibling insertions through a listener because Qt needs beginInsertRows() *before* the mutation while GTK emits row-inserted *after*; one traversal satisfies both. The Qt model is now a view onto it and still passes all 61 existing checks, which is what establishes the shared tree behaves as the verified one did. * EntryFormat, the cell text. Sizes and timestamps stay locale-aware via QLocale on the Qt side; everything where a wrong string would mislead -- mode bits, ratio, CRC, blank-versus-zero -- comes from the core. The GTK model is a hand-written GtkTreeModel over EntryTree, not a GtkTreeStore. A GtkTreeStore copies every cell into itself, which for a 100k-entry archive duplicates everything the tree already holds -- the same mistake the predecessor made with QTableWidget, in another toolkit. Node pointers are stable for the tree's lifetime, so ITERS_PERSIST is honest, and the view runs in fixed-height mode so row heights are not measured 100k times. Scanner callbacks arrive on a worker thread and GTK may only be touched from the main loop, so each one is bounced through g_idle_add. A queued handler can outlive the view, so the payload holds a weak_ptr and a destroyed view makes the handler a no-op rather than a use-after-free. Verified by tests/gtk_host.cpp, the GTK counterpart to wlx_host: it dlopen()s the built .wlx and walks the model through the public GtkTreeModel API as GtkTreeView would, checking get_path/get_iter round-trip -- the pair a hand-written model most easily gets wrong. It lists 100100 rows from many.zip in 1.75s, reloads through ListLoadNext, declines non-archives, and survives null handles and teardown. Suite is now 62 checks. Scope is listing: extraction, drag-out and the filter box remain Qt-only for now. Neither variant links the other's toolkit -- verified with ldd.
Brings the GTK3 variant level with the Qt6 one: live filter box, context menu, extract-selection-to-directory, open-with-default-application, and drag-out as text/uri-list. Extraction and drag go through the same archiveview::Extractor, so the three layers of path-containment checks are the ones already tested, not a second implementation. Nested extraction uses a GMainLoop rather than a `while (...) gtk_main_iteration()` spin, for the reason the Qt variant now does the same: a spin has no exit if the terminal callback never arrives, so a failure to report presents as a frozen file manager instead of an error. The same re-entrancy guard is here too. Two bugs found by the GTK host harness, both real: * Row duplication. EntryTree attached a whole batch and announced it afterwards, which suits Qt (beginInsertRows must precede the mutation) but violates GtkTreeModel: a model must not expose a row before announcing it, because GtkTreeModelFilter builds its level cache from row-inserted *and* independently enumerates what the model already holds. A 110-row fixture reported 217 through the filter, then 321 when the emission was made recursive. EntryTree now has two modes -- Grouped for Qt, Immediate for GTK, attaching and announcing one node at a time, parent before child. 110 = 110. * Quadratic scan cost. With the filter permanently between the view and the model, every insertion reindexed a filter level: ~1.1 s of overhead at 10k rows, extrapolating past ten minutes at 100k, which is how it was found -- the suite timed out. The filter is now attached only while something is typed in the box. 100k rows list in 2.8 s. Also drops EXT="ACE" from both variants. libarchive ships no ACE reader, so the extension only bought a probe that always declined. Reading ACE would mean the unace decoder: a binary-only tool rather than a library, proprietary-licensed, unmaintained since ~2005, and the codebase behind CVE-2018-20250 -- the traversal RCE that made WinRAR drop ACE support outright. Not something to statically link into a file manager's process. Suite is 62 checks; the GTK case now also asserts the filter narrows and restores, and that the drag source advertises text/uri-list.
Selecting a directory and extracting produced only the directory itself. The Qt widget found children by walking the view's rows, which looks equivalent to walking the tree and is not. Qt returns selections in the name column, and only column-0 indexes have children -- rowCount() on a column-1 parent is 0 by contract, and ArchiveModel honours that. So the descent terminated immediately. Demonstrated on a model holding one directory with three children: rowCount is 3 at column 0 and 0 at column 1, which is the index selectedRows(NameColumn) hands back. Normalising the column would have fixed the reported symptom and left two siblings of it in place, because a view walk is the wrong source of truth here: it also sees nothing beneath a row in flat mode, and silently skips children hidden by an active filter. What gets written to disk should not depend on what happens to be on screen. Resolution moved to EntryTree::membersUnder(), which expands the selected paths against the whole tree, and both variants now call it. Prefix matching respects the path boundary, so selecting "dir" does not drag in "dirty.txt". Adds tests/select_smoke.cpp and four suite cases over real fixtures: 5000 files under one directory, a directory with a stored entry of its own (1001, not 1000 -- zip -r writes dir7/ alongside its files), a 300-level chain, and the boundary case. End to end, extracting the 5000-member selection puts 5000 files on disk. Suite is 66 checks.
Double-clicking a member now opens it with the default application, in both variants. Directories keep the view's expand/collapse behaviour -- the test is on the entry type rather than on whether an entry record exists, because a directory can carry one. The GTK variant hooks row-activated, so Enter works the same way. The context menu's "Open with default application" becomes a plain "Open", joined by a real "Open with…" chooser. For the chooser the Qt variant calls org.freedesktop.portal.OpenURI.OpenFile with ask=true. That is the portable way to get the *desktop's own* dialog: the portal frontend is a freedesktop standard and each desktop ships its own backend (xdg-desktop-portal-kde, -gtk, -gnome, -hyprland). Qt has no app chooser API, and the alternative -- parsing .desktop files and launching them -- means reimplementing Exec= field expansion and needs QProcess, which cmake/CheckNoSubprocess.cmake forbids for good reason. The file goes over D-Bus as a file descriptor, which is what the interface takes and what lets the portal reach a file in our scratch directory. The call is bounded at two seconds: a portal frontend with no backend installed can leave a request unanswered, and blocking a file manager on that is the failure mode this plugin exists to avoid. When no portal answers, the plugin says so instead of silently launching the default application -- that is not what "Open with…" was asked to do. Verified: OpenURI version 5 responds here, and a call to an absent service is detected as ServiceUnknown, which is the fallback path. The GTK variant uses GtkAppChooserDialog, which is already the desktop's chooser on a GTK desktop and needs no portal at all. Adds Qt6::DBus to the Qt variant. No subprocess is introduced; the guard target still passes.
The columns were laid out at fixed widths and, when DC passed lcp_fittowindow, resized to their contents -- so a listing docked in a wide panel stopped partway across it and left the rest empty. Reported with a screenshot: roughly 580px of a 1370px pane unused, with member names ellipsized to four characters while all that space sat idle. The name column now takes whatever width the others leave over. It is the right column to widen: it is the one that gets ellipsized, and the only one whose useful length varies per archive. A 200px floor keeps it readable when the metadata columns alone overflow a narrow pane, in which case the view scrolls horizontally as before -- hiding columns through the HiddenColumns ini key is the fix for that. Refitting happens on three occasions, and the third is the subtle one: the widget resizing, the first batch of entries arriving, and the viewport resizing. The viewport narrows on its own when the vertical scrollbar appears partway through a large scan, after the widget itself has stopped resizing; without watching for that the columns overshoot by the scrollbar's width and the view scrolls for no reason. Measured on a 100k-entry archive: 1368 used against a 1356px viewport before, exact after. The GTK variant gets the same behaviour from gtk_tree_view_column_set_expand on the name column. wlx_host now sizes the plugin widget to its container the way DC does -- without that the widget kept its default size and any width assertion was meaningless -- and reports used-versus-available width. The suite fails if a listing leaves more than a few pixels of the pane unused.
…ne edition Squashed merge of markdownview-vegalite. Squashed rather than merged so that the branch's intermediate Matplot++ step (5709943, which vendored ~1.07M lines under wlx/markdownview/3rdparty/matplotplusplus/) does not become reachable from master. Later commits on the branch removed that renderer again, so the tree here is identical to a normal merge -- only the vendored blob's ancestry is dropped. Squashed commits: dad317a markdownview: render figures from Vega-Lite specs 5f83f76 markdownview: fix GTK3 build -- missing fcfreetype.h include 4c24c39 markdownview: ship light and vlcharts editions 7c592a4 markdownview: document diagram network dependency, drop unused md4qt 4229777 markdownview: handover notes for the online/offline edition split 106d9bb markdownview: per-notation diagram services, offline edition, HiDPI fix
The repository had no .gitignore, which is how wlx/archiveview/build_tsan (815 files, 23.4 MB of ThreadSanitizer build output) ended up committed. Rules are scoped deliberately rather than by broad extension globs, since several checked-in fixtures would otherwise be caught: wfx/rclone/build/ holds build scripts, plugman/testdata carries a sample .wlx, wlx/logview ships sample.log, and MicroTeX keeps real sources under lib/.
The plugin's .gitignore covered build/ and build_asan/ but not build_tsan/, which is how that build tree came to be committed on this branch. Carried over from PR #3; the build output itself is no longer in this branch's history, so only the ignore rule is needed here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I've built GTK versions of almost all the plugins I built for Qt. I don't use them, only QT. Why did I do it? Because the GTK community is large, doublemd deserves to be the most popular file manager out there!!! However, there's such a powerful capability in plugins that I believe you are severely underselling it if you don't advertise them properly and don't stimulate people to build plugins for it. Total Commander has a huge page where you can search for plugins and all. Finding plugins for Double Commander is tough. Please merge into my fork.
Look at what my fork does when you run the CI (check for yourself here https://github.com/pplupo/doublecmd_plugins/releases/tag/v0.01):
Plugins in this release
Each plugin is packaged as its own zip below -- grab only the ones you need.